import { ArgumentException } from './exceptions/ArgumentException'

const _key = Symbol('messageKey')
const _rawText = Symbol('messageText')
const _defaultText = Symbol('messageDefaultText')
const _replacers = Symbol('messageReplacers')
const _resolver = Symbol('messageTextResolver')

export class LocaleText {
  private [_key]: string
  private [_rawText]: string
  private [_defaultText]: string
  private [_replacers]: Readonly<Record<string, unknown>> | ReadonlyArray<unknown>
  private [_resolver]: (key: string, defaultText: string | null) => string

  constructor(
    key: string,
    defaultText: string | null,
    replacers?: Readonly<Record<string, unknown>> | ReadonlyArray<unknown>,
    textResolver?: (key: string, defaultText: string) => string
  ) {
    this[_key] = key?.trim()
    this[_defaultText] = defaultText ?? this[_key]
    if (!this[_defaultText]?.trim()) throw new Error('Locale.key and Locale.defaultText cannot empty in both')
    this[_replacers] = replacers
    this[_resolver] = textResolver ?? ((id, defaultText) => defaultText ?? id)
  }

  get messageKey(): string {
    return this.messageKey
  }
  get messageRawText(): string {
    if (this[_rawText] === undefined) {
      if (!this[_key]) return (this[_rawText] = this[_defaultText])
      let rawText = this[_resolver](this[_key], this[_defaultText])
      if (!rawText?.trim()) rawText = this[_defaultText]
      this[_rawText] = rawText
    }
    return this[_rawText]
  }

  get messageTextResolver(): (key: string, defaultText: string | null) => string {
    return this[_resolver]
  }

  get messageTextPlaceholderValues(): Readonly<Record<string, unknown>> | ReadonlyArray<unknown> {
    return this[_replacers]
  }

  getText(): string {
    var replacers = this[_replacers]
    var rawText = this.messageRawText
    if (!replacers || typeof replacers !== 'object') return rawText
    if (Array.isArray(replacers)) {
      return rawText.replace(/\{(\d+)\}/, (partten, group) => {
        return replacers[group]
      })
    } else {
      return rawText.replace(/\{([^\}]+)\}/, (partten, group) => {
        return replacers[group]
      })
    }
  }
  toString() {
    return this.getText()
  }
  toJSON() {
    return this.getText()
  }
}

const _texts = Symbol('texts')
const _language = Symbol('language')
const _textLoader = Symbol('loader')
export class Locale {
  [_texts]: Record<string, string>;
  [_language]: string;
  [_textLoader]: Promise<Locale>
  constructor(language: string, initial: Record<string, unknown>, loader?: Promise<Record<string, unknown>> | { (lang): Promise<Record<string, unknown>> }) {
    if (language === undefined || language === null) {
      throw new Error('Local.language is required')
    }
    language = language?.trim()

    this[_language] = language
    if (initial) this[_texts] = plainKeys(initial)
    else this[_texts] = {}
    if (loader) {
      if (typeof (loader as Promise<Record<string, unknown>>).then === 'function') {
        this[_textLoader] = new Promise<Locale>((resolve, reject) => {
          ;(loader as Promise<Record<string, unknown>>).then((rawData) => {
            this[_texts] = plainKeys(rawData, this[_texts])
            resolve(this)
          }, reject)
        })
      } else {
        ;(loader as Promise<Record<string, unknown>>).then((rawData) => {
          this[_texts] = plainKeys(rawData, this[_texts])
        })
      }
    } else {
      this[_textLoader] = Promise.resolve(this)
    }
  }
  ready(): Promise<Locale> {
    return this[_textLoader]
  }

  getRawText(key: string, defaultText?: string) {
    return this[_texts][key] ?? defaultText ?? key
  }

  makeLocaleText(key: string, defaultText: string | null, replacers?: Readonly<Record<string, unknown>> | ReadonlyArray<unknown>) {
    return new LocaleText(key, defaultText, replacers, (id, dft) => this.getRawText(id, dft))
  }
}
function plainKeys(currentData: Record<string, unknown>, target?: Record<string, string>, prefix?: string): Record<string, string> {
  target || (target = {})
  if (!currentData) return target
  for (let member in currentData) {
    let value = currentData[member]
    if (!value) throw new Error(`localeData.${member} cannot be empty`)
    var key = prefix ? prefix + '.' + member : member
    var t = typeof value
    if (t === 'object') {
      plainKeys(value as Record<string, unknown>, target, key)
    } else {
      value = value.toString().trim()
      if (!value) throw new Error(`localeData.${member} cannot be empty`)

      target[key] = value as string
    }
  }
  return target
}
const _languages = Symbol('languages')
const _supports = Symbol('supports')
export class Localization {
  [_languages]: Record<string, Locale>;
  [_supports]: ReadonlyArray<string>;
  [_textLoader]: { (lang): Promise<Record<string, unknown>> }
  constructor(supportedLanguages: ReadonlyArray<string>, loader: { (lang): Promise<Record<string, unknown>> }) {
    this[_supports] = supportedLanguages
    this[_textLoader] = loader
    this[_languages] = {}
  }
  load(language: string, initial?: Record<string, unknown>): Promise<Locale> {
    return this.getLocale(language, initial).ready()
  }

  getLocale(language: string, initial?: Record<string, unknown>): Locale {
    let locale = this[_languages][language]
    if (locale) return locale
    if (this[_supports] && !this[_supports].some((p) => p == language)) throw new Error(`language ${language} is not supported`)
    return (this[_languages][language] = new Locale(language, initial, this[_textLoader]))
  }
  config(language: string, initial?: Record<string, unknown>): Locale {
    return (currentLocale = this.getLocale(language, initial))
  }
  makeLocaleText(key: string, defaultText: string | null, replacers?: Readonly<Record<string, unknown>> | ReadonlyArray<unknown>) {
    return currentLocale.makeLocaleText(key, defaultText, replacers)
  }
}
let currentLocale: Locale
const messages = {
  errors: {
    argument: 'argument {0} has error',
    required: '{0} is required',
    empty: '{0} cannot be empty',
    'not-supported': 'not support {0}',
  },
}
